package com.zentrale1.noodlemail;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashMap;
import java.util.Properties;

import net.contentobjects.jnotify.JNotify;
import net.contentobjects.jnotify.JNotifyException;
import net.contentobjects.jnotify.JNotifyListener;

import com.zentrale1.noodlemail.actions.Action;
import com.zentrale1.noodlemail.classifiers.NoodleClassifier;
import com.zentrale1.noodlemail.classifiers.Result;
/*
 * The MIT License
 * 
 * Copyright (c) 2010 Andreas Fleig
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 */


public class Noodlemail 
{
	private NoodleClassifier classifier;
	private Maildir maildir;
	static final int SLEEP_TIME = 600000; // 10 minutes
	static final String CLASSIFIER_PROPERTY = "classifier";
	static final String ACTION_PROPERTY_SUFFIX = ".action";
	static final String DEFAULT_HOSTNAME = "localhost";
	static final String INBOX_NAME = "inbox";
	static final String DEFAULT_CLASSIFIER = "noodle";
	private static String hostname;
	private Properties properties;
	private HashMap<String, Action> folderActions;
	private String[] ignoredFolders;
	
	public static void main(String[] args)
	{
		if (args.length < 1) {
			System.out.println("invocation: Noodlemail </path/to/Maildir>");
			return;
		}
		
		// load properties
		Properties p = new Properties();
		try {
			FileInputStream fip = new FileInputStream("noodlemail.properties");
			p.load(fip);
			fip.close();
		} catch (FileNotFoundException e1) {
			e1.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
		
		Noodlemail n = new Noodlemail(args[0], p);
		n.train();
		
		n.assignActions();
		
		log("Waiting for new mails...");
		n.watch();
		
		while (true) {
			try {
				Thread.sleep(SLEEP_TIME);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
	
	
	/**
	 * Creates a Noodlemail instance.
	 * @param dir path to a maildir
	 * @param p Properties to use
	 */
	public Noodlemail(String dir, Properties p)
	{
		this.properties = p;
		this.maildir = new Maildir(dir);
		folderActions = new HashMap<String, Action>();
		setHostname();
		loadIgnoredFolders();
		loadClassifier();
	}
	
	
	/**
	 * Returns array of all folder names
	 * @return array of all folder names
	 */
	public String[] listFolders()
	{
		Maildir[] subFolders = maildir.getFolders();
		ArrayList<String> folderList = new ArrayList<String>();
		folderList.add(INBOX_NAME);
		for (Maildir m : subFolders) {
			if (!isIgnoredFolder(m.getName()))
				folderList.add(m.getName());
		}
		return folderList.toArray(new String[folderList.size()]);
	}
	
	
	public Properties getProperties()
	{
		return properties;
	}
	
	
	public String getHostname()
	{
		return hostname;
	}
	
	
	/**
	 * Loads all action assignments from the properties file,
	 * initializes the actions, and maps them to the folders
	 */
	private void assignActions()
	{
		// get list of folders
		String[] folderList = listFolders();
		
		// get actions for each folder from properties
		// put Action instance or null in folderActions map 
		HashMap<String, Action> actionMap = new HashMap<String, Action>();
		for (String folder : folderList) {
			Action action = null;
			String actionName = properties.getProperty(folder
					+ ACTION_PROPERTY_SUFFIX);
			if (actionName != null) {
				if (!actionMap.containsKey(actionName)) {
					action = ActionFactory.getAction(actionName, this);
					actionMap.put(actionName, action);
				} else {
					action = actionMap.get(actionName);
				}
				
			}
			folderActions.put(folder, action);
		}
	}
	
	
	/**
	 * Loads the classifier defined in the properties file
	 */
	private void loadClassifier()
	{
		String cName = properties.getProperty(CLASSIFIER_PROPERTY);
		if (cName == null)
			cName = DEFAULT_CLASSIFIER;
		
		classifier = ClassifierFactory.getClassifier(cName, listFolders());
		
		if (classifier == null) {
			log("No classifier selected, exiting");
			System.exit(0);
		}
		
		log("Using classifier: "+classifier.getName());
	}
	
	
	/**
	 * Starts a JNotify watch on the Maildir
	 */
	public void watch()
	{
		String dir = maildir.getPath();
		try {
			createWatch(dir);
		} catch (JNotifyException e) {
			e.printStackTrace();
		}
	}
	
	
	/**
	 * Loads the ignore list from the properties object
	 */
	private void loadIgnoredFolders()
	{
		String i = properties.getProperty("ignore");
		if (i == null) {
			ignoredFolders = new String[0];
		} else {
			ignoredFolders = i.split(" ");
		}
	}
	
	
	/**
	 * Checks if a folder is in the ignore list
	 * @param folderName name of the folder
	 * @return true, if it is in the ignore list
	 */
	private boolean isIgnoredFolder(String folderName)
	{
		for (String f : ignoredFolders) {
			if (f.equals(folderName))
				return true;
		}
		return false;
	}
	
	
	/**
	 * Trains the classifier on all messages in the maildir and its subfolders 
	 */
	public void train()
	{
		log("Training classifier...");
		trainDir(maildir, INBOX_NAME);
		trainSubdirs(maildir);
	}
	
	
	/**
	 * Runs trainDir on all subfolders of a Maildir
	 * @param dir a Maildir
	 */
	private void trainSubdirs(Maildir dir)
	{
		Maildir[] folders = dir.getFolders();
		for (Maildir folder : folders) {
			String[] pathElements = folder.getPath().split("/");
			String name = pathElements[pathElements.length-1];
			if (!isIgnoredFolder(name)) {
				//log("training "+name);
				trainDir(folder, name);
			}
		}
	}
	
	
	/**
	 * Trains the classifier on all files in a Maildir, but not the 
	 * subdirectories.  
	 * @param dir a Maildir
	 * @param category name of the Maildir (this should be the basename)
	 */
	private void trainDir(Maildir dir, String category)
	{
		String[] paths = dir.listMessages(false);
		for (String path : paths) {
			Mail m = MailFactory.parse(path);
			classifier.train(m, category);
		}
	}
	
	
	/**
	 * Runs the classifier on a mail file
	 * @param path path to the mail file
	 * @return most probable class
	 */
	private String classify(String path)
	{
		Mail m = MailFactory.parse(path);
		Result[] results = classifier.classify(m);
		return results[0].getCategory();
	}
	

	/**
	 * Gets the folder name a message file is in
	 * @param filename filename of the message
	 * @return name of the folder
	 */
	private String getFolderOfFile(String filename)
	{
		File newFile = new File(filename);
		String parent = newFile.getParentFile().getName();
		if (parent != null && (parent.equals("new") || parent.equals("cur") 
				|| parent.equals("tmp")))
		{
			parent = newFile.getParentFile().getParentFile().getName();
		}
		return parent;
	}
	
	
	/**
	 * Creates a JNotify watch on a path
	 * @param path path to watch
	 * @return JNotify watch id
	 * @throws JNotifyException
	 */
	private int createWatch(String path) throws JNotifyException
	{
		int mask = JNotify.FILE_CREATED;
		boolean watchSubtree = true;
		int watchId = JNotify.addWatch(path, mask, watchSubtree, 
				new JNotifyListener()
		{
			@Override
			public void fileCreated(int wd, String root, String name) 
			{
				String filename = root + "/" + name;
				String parent = new File(filename).getParentFile().getName();
				
				// check ignore list
				if (!isIgnoredFolder(getFolderOfFile(filename)) &&
						!parent.equals("tmp")) {
					log("* New file: "+name);
					// classify
					String folder = classify(filename);
					log("  Classified as "+folder);
					
					// perform action
					if (folderActions.containsKey(folder)) {
						Action a = folderActions.get(folder);
						if (a == null) {
							log("  No action defined for "+folder);
						} else {
							a.perform(name, folder);
						}
					} else {
						log("Error: folderActions has no entry for "+folder);
					}
				}
			}

			@Override
			public void fileDeleted(int arg0, String arg1, String arg2) 
			{	
			}

			@Override
			public void fileModified(int arg0, String arg1, String arg2) 
			{
			}

			@Override
			public void fileRenamed(int arg0, String arg1, String arg2,
					String arg3) 
			{
				// called when mail is moved (e.g. to another directory)
				// might be used to modify the classifier model
			}
			
		});
		return watchId;
	}
	
	
	public Maildir getMaildir()
	{
		return maildir;
	}
	
	
	/**
	 * Creates a syslog-style timestamp
	 * @return timestamp
	 */
	private static String getTimestamp()
	{
		Calendar c = Calendar.getInstance();
		int dom = c.get(Calendar.DAY_OF_MONTH);
		String[] months = {
				"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug",
				"Sep", "Oct", "Nov", "Dec"
		};
		String mon = months[c.get(Calendar.MONTH)];
		int hour = c.get(Calendar.HOUR_OF_DAY);
		int min = c.get(Calendar.MINUTE);
		int sec = c.get(Calendar.SECOND);
		String space;
		if (dom < 10) {
			space = "  ";
		} else {
			space = " ";
		}
		return mon + space + dom + " " + formatInt(hour) + ":" + formatInt(min)
			+ ":" + formatInt(sec);
	}
	
	
	/**
	 * Formats an int to at least 2 digits 
	 * @param i
	 * @return
	 */
	private static String formatInt(int i)
	{
		if (i < 10)
			return "0"+i;
		return ""+i;
	}
	
	
	/**
	 * Sets the hostname
	 */
	private static void setHostname()
	{
		try {
			hostname = InetAddress.getLocalHost().getHostName();
		} catch (UnknownHostException e1) {
			e1.printStackTrace();
			hostname = DEFAULT_HOSTNAME;
		}
	}
	
	
	/**
	 * Prints a message to STDOUT, syslog-style 
	 * @param message message to print
	 */
	public static void log(String message)
	{
		if (hostname == null)
			setHostname();
		String msg = getTimestamp() + " " + hostname + " Noodlemail: "+ message;
		System.out.println(msg);
	}

}
